using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace TankWars
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        #region Network Variables
        const int maxGamers = 16;
        const int maxLocalGamers = 4;
        bool signedIn = false;

        NetworkSession networkSession;

        PacketWriter packetWriter = new PacketWriter();
        PacketReader packetReader = new PacketReader();

        string errorMessage;
        #endregion

        #region Graphic Variables
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        private SpriteFont font;
        private Texture2D statusAreaBorderTexture;

        private Texture2D TankTexture;
        private Dictionary<Type, Texture2D> dicShotTextures = new Dictionary<Type, Texture2D>();
        private Dictionary<Type, Texture2D> dicShotIcons = new Dictionary<Type, Texture2D>();
        private Texture2D[] aryMapTextures;

        //Field textures.
        private Texture2D blockTexture;

        private Texture2D mapTexture;
        #endregion

        #region Game Variables
        private Player mainPlayer;
        private Player testPlayer;

        private TankManager tankManager;
        private GameField field;

        //private Texture2D mousePointer;
        private Vector2 mousePosition;
        private MouseState mouseState;

        private Map gameMap;

        private StatusArea statusArea;
        #endregion

        #region Debug Variables
        private FPS fps;
        #endregion

        //private AudioEngine audioEngine;
        //private WaveBank waveBank;
        //private SoundBank soundBank;

        #region Constructor
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            //Add gamer service componet to enable networking functionality.
            Components.Add(new GamerServicesComponent(this));
        }
        #endregion

        #region Initialization and Loading
        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here

            //sound initialization -- This points to the XACT project, sound bank, and wave bank bank file.
            //audioEngine = new AudioEngine("Content\\Audio\\TankWarsAudio.xgs");
            //waveBank = new WaveBank(audioEngine, "Content\\Audio\\Wave Bank.xwb");
            //soundBank = new SoundBank(audioEngine, "Content\\Audio\\Sound Bank.xsb");

            // Initializes frames per second updating in the title bar

#if DEBUG
            fps = new FPS(this);
            Components.Add(fps);
#endif

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            
            //Load sprite fonts
            this.font = Content.Load<SpriteFont>("RockwellFont");

            #region Load all textures
            this.statusAreaBorderTexture = Content.Load<Texture2D>("StatusAreaBorder");

            this.TankTexture = Content.Load<Texture2D>("tankbase2");

            

            //Load all map textures
            aryMapTextures = new Texture2D[2];
            //aryMapTextures[0] = Content.Load<Texture2D>("tank2");//Spawn point may or may not have a graphic.
            aryMapTextures[1] = Content.Load<Texture2D>("MapObjects\\obj1");
            field = new GameField(aryMapTextures, this.spriteBatch);

            //Load all shot textures
            this.dicShotTextures.Add(typeof(Weapons.Bullet), Content.Load<Texture2D>("Weapons/BulletSmall"));
            this.dicShotTextures.Add(typeof(Weapons.PlasmaBall), Content.Load<Texture2D>("Weapons/PlasmaBallSmall"));
            this.dicShotTextures.Add(typeof(Weapons.Laser), Content.Load<Texture2D>("Weapons/LaserSmall"));
            this.dicShotTextures.Add(typeof(Weapons.Missile), Content.Load<Texture2D>("Weapons/MissileSmall"));
            this.dicShotTextures.Add(typeof(Weapons.Mine), Content.Load<Texture2D>("Weapons/MineSmall"));

            //Load all shot icons
            this.dicShotIcons.Add(typeof(Weapons.Bullet), Content.Load<Texture2D>("Weapons/Bullet"));
            this.dicShotIcons.Add(typeof(Weapons.PlasmaBall), Content.Load<Texture2D>("Weapons/PlasmaBall"));
            this.dicShotIcons.Add(typeof(Weapons.Laser), Content.Load<Texture2D>("Weapons/Laser"));
            this.dicShotIcons.Add(typeof(Weapons.Missile), Content.Load<Texture2D>("Weapons/Missile"));
            this.dicShotIcons.Add(typeof(Weapons.Mine), Content.Load<Texture2D>("Weapons/Mine"));

            String mapName = "Level4";
            this.mapTexture = Content.Load<Texture2D>(mapName);
            field.setUpLevel(mapName,mapTexture.Width,mapTexture.Height);
            //this.mapTexture = Content.Load<Texture2D>("map1");
            #endregion

            // TODO: use this.Content to load your game content here
            this.tankManager = new TankManager(this.TankTexture, this.Content.Load<Texture2D>("tankturret2"), this.dicShotTextures, this.dicShotIcons, this.spriteBatch);
            this.mainPlayer = new Player("");
            this.mainPlayer.setTank(new Tank(this.TankTexture, this.Content.Load<Texture2D>("tankturret2"), mainPlayer, this.dicShotTextures, this.dicShotIcons));
            this.mainPlayer.Tank.vPosition = new Vector2(100.0f, 100.0f);
            //this.tankManager.NewTank(this.mainPlayer);
            //this.tankManager.dicTanks[this.mainPlayer].vPosition = new Vector2(400.0f, -200.0f);
            
            this.testPlayer = new Player("test");
            this.tankManager.NewTank(this.testPlayer, Color.Red);
            this.tankManager.dicTanks[this.testPlayer].vPosition = new Vector2(1000.0f, 100.0f);
            
            this.statusArea = new StatusArea(this.statusAreaBorderTexture, this.font, mainPlayer.Tank, this.graphics.GraphicsDevice.Viewport);
            this.graphics.GraphicsDevice.Viewport = this.statusArea.ResizeViewport(this.graphics.GraphicsDevice.Viewport);

            //this.mousePointer = Content.Load<Texture2D>("Mouse");

            //soundBank.PlayCue("MENU");

            this.gameMap = new Map(this.mapTexture, mainPlayer.Tank, this.graphics.GraphicsDevice.Viewport);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }
        #endregion

        #region Update Methods
        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            //If session has not yet been created.
            if (networkSession==null)
            {
                updateMenuScreen();
            }
            else
            {
                updateGameSession(gameTime);
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Updates the menu screen while its opened.
        /// </summary>
        public void updateMenuScreen()
        {
            //Try to join or create a session once the window comes up.
            if (IsActive)
            {
                if (!signedIn)
                {
                    signedIn = true;
                    // If there are no profiles signed in, we cannot proceed.
                    // Show the Guide so the user can sign in.
                    Guide.ShowSignIn(maxLocalGamers, false);
                }

                DrawMessage("Left click to create a session and right click to join a session.");
                mouseState = Mouse.GetState();

                if (mouseState.LeftButton == ButtonState.Pressed)
                {
                    createSession();
                }
                else if (mouseState.RightButton == ButtonState.Pressed)
                {
                    //Try to join three times.
                    int connectAttempts = 0;
                    while ((networkSession == null)&&(connectAttempts<3))
                    {
                        joinSession(connectAttempts);
                        connectAttempts++;
                    }
                }
            }//If: (IsActive)

        }//Method: UpdateMenuScreen

        /// <summary>
        /// Updates the mechanics of the game.
        /// </summary>
        public void updateGameSession(GameTime gameTime)
        {
            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // Read input for local players, and sends them to the server.
            //If this is the server we just read local player's input.
            foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
            {//For loop not needed since the current version can have only 1 local gamer.
                sendPacketsToServer(gamer);
            }

            // If we are the server, update all the tanks and transmit
            // their latest positions back out over the network.
            if (networkSession.IsHost)
            {
                sendPacketsToClient(gameTime);
            }

            // Pump the underlying session object.
            networkSession.Update();

            // Make sure the session has not ended.
            if (networkSession == null)
                return;

            //Read packets for all gamers.
            foreach (LocalNetworkGamer gamer in networkSession.LocalGamers)
            {
                if (gamer.IsHost)
                {
                    recievePacketsFromClients(gamer);
                }
                else
                {
                    recievePacketsFromServer(gamer);
                }
            }
        }
        #endregion

        #region Network Functions

        ///<summary>
        ///Attepts to create a network session
        ///</summary>
        public void createSession()
        {
            DrawMessage("Creating session...");

            try
            {
                networkSession = NetworkSession.Create(NetworkSessionType.SystemLink,
                                                       maxLocalGamers, maxGamers);
                tankManager.setNetwork(networkSession);
                
                HookSessionEvents();
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
            }
        }

        /// <summary>
        /// Attempts to join a server running a game session.
        /// </summary>
        public bool joinSession(int attempts)
        {
            DrawMessage("Searching for first available game session.  Attempt Number:"+attempts);

            try
            {
                // Search for sessions.
                using (AvailableNetworkSessionCollection availableSessions =
                            NetworkSession.Find(NetworkSessionType.SystemLink,
                                                maxLocalGamers, null))
                {
                    if (availableSessions.Count == 0)
                    {
                        errorMessage = "No network sessions found.";
                        return false;
                    }

                    // Join the first session we found.
                    networkSession = NetworkSession.Join(availableSessions[0]);
                    tankManager.setNetwork(networkSession);

                    HookSessionEvents();
                    return true;
                }
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
                return false;
            }
        }

        /// <summary>
        /// After creating or joining a network session, we must subscribe to
        /// some events so we will be notified when the session changes state.
        /// </summary>
        void HookSessionEvents()
        {
            networkSession.GamerJoined += GamerJoinedEventHandler;
            networkSession.SessionEnded += SessionEndedEventHandler;
        }

        /// <summary>
        /// This event handler will be called whenever a new gamer joins the session.
        /// We use it to allocate a Tank object, and associate it with the new gamer.
        /// </summary>
        void GamerJoinedEventHandler(object sender, GamerJoinedEventArgs e)
        {
            int gamerIndex = networkSession.AllGamers.IndexOf(e.Gamer);

            if (e.Gamer.IsLocal)
            {
                e.Gamer.Tag = this.mainPlayer;
                mainPlayer.Tank.vPosition = field.getSpawnPoint();
            }
            else//Create a new tank if it is not local
            {
                //Create a new player.
                Player joiningPlayer = new Player("Player#" + gamerIndex);
                joiningPlayer.setTank(new Tank(this.TankTexture, this.Content.Load<Texture2D>("tankturret2"), mainPlayer, this.dicShotTextures, this.dicShotIcons));
                joiningPlayer.Tank.vPosition = field.getSpawnPoint();
                e.Gamer.Tag = joiningPlayer;
            }
        }

        /// <summary>
        /// Event handler notifies us when the network session has ended.
        /// </summary>
        void SessionEndedEventHandler(object sender, NetworkSessionEndedEventArgs e)
        {
            errorMessage = e.EndReason.ToString();

            networkSession.Dispose();
            networkSession = null;
        }

        public void sendPacketsToClient(GameTime gameTime)
        {
            //Update the tanks on the server.
            this.tankManager.Update(gameTime, Mouse.GetState(), this.gameMap);
            this.tankManager.checkFieldCollisions(this.mainPlayer,field);

            //Now send the update to the clients.

            // First off, our packet will indicate how many tanks it has data for.
            packetWriter.Write(networkSession.AllGamers.Count);

            // Loop over all the players in the session, not just the local ones!
            foreach (NetworkGamer gamer in networkSession.AllGamers)
            {
                // Look up what tank is associated with this player.
                Player anyPlayer = gamer.Tag as Player;

                // Write the tank state into the output network packet.
                packetWriter.Write(anyPlayer.Tank.vPosition);
                packetWriter.Write(anyPlayer.Tank.fRotation);
                packetWriter.Write(anyPlayer.Tank.Turret.fRotation);

                packetWriter.Write(anyPlayer.Tank.WeaponManager.PrimaryShotsFired.Count);
                foreach (Weapons.Shot currentShot in anyPlayer.Tank.WeaponManager.PrimaryShotsFired)
                {
                    packetWriter.Write(currentShot.vPosition);
                    packetWriter.Write(currentShot.fRotation);
                }
            }

            // Send the combined data for all tanks to everyone in the session.
            LocalNetworkGamer server = (LocalNetworkGamer)networkSession.Host;
            server.SendData(packetWriter, SendDataOptions.InOrder);
        }

        ///<summary>
        /// Function called by client to send data to the server.
        /// </summary>
        public void sendPacketsToServer(LocalNetworkGamer gamer)
        {
            //The passed in parameter is a "local gamer".  Therefore
            //look up the player associated with this gamer and get
            //the inputs.
            Player localPlayer = gamer.Tag as Player;
            getPlayerInput(localPlayer);
            localPlayer.Tank.Turret.UpdatePosition(Mouse.GetState(),this.gameMap);

            // Only send if we are not the server. There is no point sending packets
            // to ourselves, because we already know what they will contain!
            if (!networkSession.IsHost)
            {
                // Write our latest input state into a network packet.
                packetWriter.Write(localPlayer.Tank.leftInput);
                packetWriter.Write(localPlayer.Tank.rightInput);
                packetWriter.Write(localPlayer.Tank.upInput);
                packetWriter.Write(localPlayer.Tank.downInput);
                packetWriter.Write(localPlayer.Tank.primaryInput);
                packetWriter.Write(localPlayer.Tank.secondaryInput);
                packetWriter.Write(localPlayer.Tank.Turret.fRotation);

                // Send our input data to the server.
                gamer.SendData(packetWriter,
                               SendDataOptions.InOrder, networkSession.Host);
            }
        }

        ///<summary>
        ///Function called by server to recieve packets from clients.
        ///</summary>
        public void recievePacketsFromClients(LocalNetworkGamer hostGamer)
        {
            // Keep reading as long as incoming packets are available.
            while (hostGamer.IsDataAvailable)
            {
                NetworkGamer sender;

                // Read a single packet from the network.
                hostGamer.ReceiveData(packetReader, out sender);

                if (!sender.IsLocal)
                {
                    // Look up the tank associated with whoever sent this packet.
                    Player remotePlayer = sender.Tag as Player;

                    // Read the latest inputs controlling this tank.
                    remotePlayer.Tank.leftInput = packetReader.ReadBoolean();
                    remotePlayer.Tank.rightInput = packetReader.ReadBoolean();
                    remotePlayer.Tank.upInput = packetReader.ReadBoolean();
                    remotePlayer.Tank.downInput = packetReader.ReadBoolean();
                    remotePlayer.Tank.primaryInput = packetReader.ReadBoolean();
                    remotePlayer.Tank.secondaryInput = packetReader.ReadBoolean();
                    remotePlayer.Tank.Turret.fRotation = packetReader.ReadSingle();
                }
            }
        }

        /// <summary>
        /// Function called by client to recieve data from server.
        /// </summary>
        public void recievePacketsFromServer(LocalNetworkGamer gamer)
        {
            // Keep reading as long as incoming packets are available.
            while (gamer.IsDataAvailable)
            {
                NetworkGamer sender;

                // Read a single packet from the network.
                gamer.ReceiveData(packetReader, out sender);

                // If a player has recently joined or left, it is possible the server
                // might have sent information about a different number of players
                // than the client currently knows about. If so, we will be unable
                // to match up which data refers to which player. The solution is
                // just to ignore the packet for now: this situation will resolve
                // itself as soon as the client gets the join/leave notification.
                if (networkSession.AllGamers.Count != packetReader.ReadInt32())
                    continue;

                // This packet contains data about all the players in the session.
                foreach (NetworkGamer remoteGamer in networkSession.AllGamers)
                {
                    Player remotePlayer = remoteGamer.Tag as Player;

                    // Read the state of this tank from the network packet.
                    remotePlayer.Tank.vPosition = packetReader.ReadVector2();
                    remotePlayer.Tank.fRotation = packetReader.ReadSingle();
                    float temp = packetReader.ReadSingle();
                    if (!remoteGamer.IsLocal)
                    {
                        remotePlayer.Tank.Turret.fRotation = temp;
                    }

                    int nNumberOfShotsFired = packetReader.ReadInt32();
                    for (int i = 0; i < nNumberOfShotsFired; i++)
                    {
                    }
                }
            }
        }
        #endregion

        #region DrawFunctions
        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            

            //Do not draw game images until the network has been initialized.
            if (networkSession == null)
                return;

            //Clear screen.
            graphics.GraphicsDevice.Clear(Color.Black);

            //Always draw map first
            this.gameMap.Draw(this.spriteBatch);

            
            this.tankManager.Draw(this.gameMap);
            this.field.drawField(this.mainPlayer,this.gameMap);
            this.statusArea.Draw(this.spriteBatch, this.graphics.GraphicsDevice, Mouse.GetState());
            base.Draw(gameTime);
            //this.spriteBatch.Begin(SpriteBlendMode.Additive);
            //this.spriteBatch.Draw(mousePointer, mousePosition, Color.White);
            //this.spriteBatch.End();
        }

        /// <summary>
        /// Helper draws notification messages before calling blocking network methods.
        /// </summary>
        void DrawMessage(string message)
        {
            if (!BeginDraw())
                return;

            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();

            spriteBatch.DrawString(font, message, new Vector2(161, 161), Color.Black);
            spriteBatch.DrawString(font, message, new Vector2(160, 160), Color.White);

            spriteBatch.End();

            EndDraw();
        }
        #endregion

        #region Input Functions
        /// <summary>
        /// Gets the keyboard input and moves the player's tank in the proper direction
        /// </summary>
        private void getPlayerInput(Player localPlayer)
        {
            KeyboardState keyboard = Keyboard.GetState();
            this.mouseState = Mouse.GetState();

            //Clear the inputs for the player.
            localPlayer.Tank.clearInput();

            #region Movement Input
            if (keyboard.IsKeyDown(Keys.Left) || keyboard.IsKeyDown(Keys.A))
            {
                localPlayer.Tank.leftInput = true;
            }
            if (keyboard.IsKeyDown(Keys.Right) || keyboard.IsKeyDown(Keys.D))
            {
                localPlayer.Tank.rightInput = true;
            }

            if (keyboard.IsKeyDown(Keys.Up) || keyboard.IsKeyDown(Keys.W))
            {
                localPlayer.Tank.upInput = true;
            }
            if (keyboard.IsKeyDown(Keys.Down) || keyboard.IsKeyDown(Keys.S))
            {
                localPlayer.Tank.downInput = true;
            }
            #endregion

            
            #region Fire Input
            if (this.mouseState.LeftButton == ButtonState.Pressed)
            {
                localPlayer.Tank.primaryInput = true;
            }
            if (this.mouseState.RightButton == ButtonState.Pressed)
            {
                localPlayer.Tank.secondaryInput = true;
            }
            #endregion

        }

        private void MoveMouse()
        {
            this.mouseState = Mouse.GetState();
            this.mousePosition.X = this.mouseState.X;
            this.mousePosition.Y = this.mouseState.Y;

            // Restrict the Mouse so that it stays inside the current display
            if (this.mousePosition.X < 0)
                this.mousePosition.X = 0;
            if (this.mousePosition.X > this.Window.ClientBounds.Width)
                this.mousePosition.X = this.Window.ClientBounds.Width;
            if (this.mousePosition.Y < 0)
                this.mousePosition.Y = 0;
            if (this.mousePosition.Y > this.Window.ClientBounds.Height)
                this.mousePosition.Y = this.Window.ClientBounds.Height;
        }
        #endregion
    }
}
